package raytracer;

import java.awt.Color;
import java.io.File;
import java.io.IOException;
import java.util.List;

import raytracer.file.*;
import raytracer.geometry.*;
import raytracer.geometry.Rotation.Axis;
import raytracer.gui.*;
import raytracer.threading.*;

@SuppressWarnings("unused")
public class Raytracer implements IRayTracerImplementation 
{
	final int numberOfThreads = 4;
	
	boolean useAA = false;
	final int AAFactor = 2;
	
    // viewing volume with infinite end
	private float fovyDegree;
    private float near;
    private float fovyRadians;
    
    // eye point==ray starting point
    private float rayEx, rayEy, rayEz;    

    private int resx, resy;        // viewport resolution
	private float h, w, aspect;   // window height, width and aspect ratio
	
	private float[] ICenter;
	
	private boolean renderShadow = false;
	
	RayTracerGui gui = new RayTracerGui(this);	
	SceneNode sceneGraph = new SceneNode();
	
	public Raytracer() throws Exception
	{

	    
	    ICenter = new float[] { 4.0f, 4.0f, 4.0f };
	    rayEx = 0.0f;
	    rayEy = 0.0f;
	    rayEz = 10.0f;
	    
	    //sceneGraph.rtObject = RTFileReader.read(I_Sphere.class, new File("data/ikugel.dat"));
	    //sceneGraph.rtObject = RTFileReader.read(T_Mesh.class, new File("data/kugel3.dat"));
	    
	    
//	    sceneGraph.transformation = new Rotation(1, new float [] {0.0115f, -3.63492f, -0.068f}, Axis.Z_AXIS);
//	    sceneGraph.rtObject = OBJFileReader.readFile(new File("data/boy.obj"));
	    
	    
	    XMLImporter xmlimport = new XMLImporter();
	    //sceneGraph = xmlimport.getSceneGraph("data/tisch2.xml");
	    
	    sceneGraph = xmlimport.getSceneGraph("data/boy_quader.xml");
	    
	    ICenter = xmlimport.getLampVector();
		float eyeVector[] = xmlimport.getEyeVector();
		
		rayEx = eyeVector[0];
		rayEy = eyeVector[1];
		rayEz = eyeVector[2];
		
		renderShadow = xmlimport.isShadowEnabled();
		useAA = xmlimport.anti_aliasing();
		
	  
	}
	
	private void setViewParameters(float fovyDegree, float near)
	{
	    // set attributes fovyDegree, fovyRadians, near
		this.fovyDegree = fovyDegree;
		this.near = near;
		this.fovyRadians = (float) (fovyDegree / 180.0f * Math.PI);
			
	    // set attributes resx, resy, aspect
	    resx = gui.getResX();
	    resy = gui.getResY();
	    aspect = (float) resx / (float) resy;

	    // set attributes h, w
	    h = (float) (2 * near * Math.tan(fovyRadians / 2));
	    w = h * aspect; 
	}
	
	@Override
	public void doRayTrace() 
	{
		RaytracerThread raytracerThreads[] = new RaytracerThread[numberOfThreads];
		Thread threads[] = new Thread[numberOfThreads];
		
		// hardcoded viewing volume with fovy and near 
	    setViewParameters(90.0f, 1.0f);
		
		sceneGraph.prepareScene(new float [] {rayEx,  rayEy, rayEz}, ICenter);
		
		for (int i = 0; i < numberOfThreads; i++)
		{
			raytracerThreads[i] = new RaytracerThread(i, numberOfThreads, this);
			threads[i] = new Thread(raytracerThreads[i]);
			threads[i].start();
		}	
	}

	@Override
	public void rayTraceThread(int rowStart, int numThreads) 
	{
		float x, y, z;                // intersection point in viewing plane
		float rayVx, rayVy, rayVz;    // ray vector
	    Color color;
		
		z = rayEz - near;
		
	    final float AASubPixel = 1.0f / AAFactor;
	    final float AAHalf = 2 * AASubPixel;
	    final int SubPixCount = AAFactor * AAFactor;
	    
	    int sumR = 0, sumG = 0, sumB = 0;
	    float traceX, traceY;  
		
		// xp, yp: pixel coordinates 
		for(int xp = rowStart; xp < resx; xp += numThreads) 
		{
			for(int yp = 0; yp < resy; ++yp) 
			{	
				
				
				if (useAA)
			    {
				    sumR = 0;
				    sumG = 0;
				    sumB = 0;
				    
				    rayVz = z - rayEz;
				    
				    for (int ax = 0; ax < AAFactor; ax++)
				    {
				    	for (int ay = 0; ay < AAFactor; ay++)
				    	{
				    		traceX = (xp - AAHalf) + ax * AASubPixel;
				    		traceY = (yp - AAHalf) + ay * AASubPixel;
				    		
							
						    // x, y: view coordinates
						    x = (float) (traceX) / (resx - 1) * w - ( (float) w / 2.0f);
						    y = (float) ((resy) - 1.0f - traceY) / (resy) * h - ( (float) h/ 2.0f);
				    		
						    // ray vector
						    rayVx = x - rayEx;
						    rayVy = y - rayEy;
						    
						   				    
						    // get color or null along the ray
						    color = traceRayAndGetColor(rayEx, rayEy, rayEz, rayVx, rayVy, rayVz);
						    
						    // if color!=null set pixel with color
						    if (color != null)
						    {
						    	sumR += color.getRed();
						    	sumG += color.getGreen();
						    	sumB += color.getBlue();
						    	//gui.setPixel(xp, yp, color.getRGB());
						    }
				    	}
				    }
				    
				    color = new Color(sumR / SubPixCount, sumG / SubPixCount, sumB / SubPixCount);
				    gui.setPixel(xp, yp, color.getRGB());
			    }
				else
				{
				
				    x = rayEx + (float) (xp) / (resx - 1) * w - ( (float) w / 2.0f);
				    y = rayEy + (float) ((resy) - 1.0d - yp) / (resy) * h - ( (float) h/ 2.0f);
				    
				    //x = y = 0;
				    
				    // ray vector
				    rayVx = x - rayEx;
				    rayVy = y - rayEy;
				    rayVz = z - rayEz;
				    
				    // get color or null along the ray
				    color = traceRayAndGetColor(rayEx, rayEy, rayEz, rayVx, rayVy, rayVz);
				    
				    // if color!=null set pixel with color
				    if (color != null)
				    {
				    	gui.setPixel(xp, yp, color.getRGB());		
				    } 	
				}
			}
		}
	}
	
	private Color traceRayAndGetColor(float rayEx, float rayEy, float rayEz, float rayVx, float rayVy, float rayVz)
	{
		ShadingParameter parameter = new ShadingParameter();
		float minT;
		float[] l = new float[3];
		float[] color = new float[3];
    	float[] v = new float[3];
             
    	// viewing vector at intersection point, normalize!
    	v[0] = -rayVx;
    	v[1] = -rayVy;
    	v[2] = -rayVz;
     
    	Toolbox.normalize(v);
		
		minT = traceRayAndGetMinT(rayEx, rayEy, rayEz, rayVx, rayVy, rayVz, parameter);
		
		// no intersection point found => return with no result
		if (parameter.object == null)
		{
			return null;
		}
		
		// implicit: only phong shading available => shade=illuminate
		if (parameter.object.getHeader().equals("IMPLICIT_SPHERE") )
		{
			// light vector at the intersection point, normalize!
            l[0] = ICenter[0] - parameter.intersectionPoint[0];
        	l[1] = ICenter[1] - parameter.intersectionPoint[1];
        	l[2] = ICenter[2] - parameter.intersectionPoint[2];
        	
        	Toolbox.normalize(l);
        	
        	PhongIllumination.illuminate(parameter.material, parameter.materialN, l, parameter.normal, v, color, false);
        	
        	return new Color(color[0], color[1], color[2]);
		}
		else if (parameter.object.getHeader().equals("TRIANGLE_MESH"))
		{
			T_Mesh mesh = (T_Mesh) parameter.object;
			float r, g, b;
			
			if (mesh.fgp == 'f' || mesh.fgp == 'F')
			{
    	    	// flat shading: use the per triangle precalculated color
    	    	r = mesh.triangleColors[parameter.triangleIndex][0];
    	    	g = mesh.triangleColors[parameter.triangleIndex][1];
	    		b = mesh.triangleColors[parameter.triangleIndex][2];
	    		
	    		return new Color(r, g, b);
			}
			
			
	   	    switch (mesh.fgp) 
    	    {    
	    	    case 'g':
	    	    case 'G':
	    	    case 'p':
    	    	case 'P':
	    	    	
	    	    	float bu, bv, bw;
	    	    	float fr, fg, fb;
	    	    	
	    	    	bu = parameter.baricentric[0];
	    	    	bv = parameter.baricentric[1];
	    	    	bw = parameter.baricentric[2];
	    	    	
	    	    	int p1Index, p2Index, p3Index;
	    	    	
	    	    	p1Index = mesh.triangles[parameter.triangleIndex][0];
	    	    	p2Index = mesh.triangles[parameter.triangleIndex][1];
	    	    	p3Index = mesh.triangles[parameter.triangleIndex][2];
	    	    	
	    	    	switch (mesh.fgp)
	    	    	{
		    	    	case 'g':
		    	    	case 'G':
		    	    		
		    	    		fr = bu * mesh.vertexColors[p3Index][0] + bv * mesh.vertexColors[p1Index][0] + bw * mesh.vertexColors[p2Index][0];
			    	    	fg = bu * mesh.vertexColors[p3Index][1] + bv * mesh.vertexColors[p1Index][1] + bw * mesh.vertexColors[p2Index][1];
			    	    	fb = bu * mesh.vertexColors[p3Index][2] + bv * mesh.vertexColors[p1Index][2] + bw * mesh.vertexColors[p2Index][2];
			    	    	
			    			if (fr > 1)
			    				fr = 1.0f;
			    			if (fg > 1)
			    				fg = 1.0f;
			    			if (fb > 1)
			    				fb = 1.0f;
			    	    	
			    	    	return new Color(fr, fg, fb);
			    	    	
		    	    	case 'p':
		    	    	case 'P':
		    	    		
		    	    		float n [] = new float[3];
		    	    		
		    	    		n[0] = bu * mesh.vertexNormals[p3Index][0] + bv * mesh.vertexNormals[p1Index][0] + bw * mesh.vertexNormals[p2Index][0];
		    	    		n[1] = bu * mesh.vertexNormals[p3Index][1] + bv * mesh.vertexNormals[p1Index][1] + bw * mesh.vertexNormals[p2Index][1];
		    	    		n[2] = bu * mesh.vertexNormals[p3Index][2] + bv * mesh.vertexNormals[p1Index][2] + bw * mesh.vertexNormals[p2Index][2];
		    	    		
		    	    		Toolbox.normalize(n);
		    	    		
		    	    		// light vector at the intersection point, normalize!
		    	            l[0] = ICenter[0] - parameter.intersectionPoint[0];
		    	        	l[1] = ICenter[1] - parameter.intersectionPoint[1];
		    	        	l[2] = ICenter[2] - parameter.intersectionPoint[2];
		    	        	
		    	        	Toolbox.normalize(l);		    	 
		    	        	
		    	        	float icenterIP[] = new float[3];
		    	    		
		    	    		icenterIP[0] = parameter.intersectionPoint[0] - ICenter[0];
		    	    		icenterIP[1] = parameter.intersectionPoint[1] - ICenter[1];
		    	    		icenterIP[2] = parameter.intersectionPoint[2] - ICenter[2];
		    	    		
		    	    		minT = 1.0f;
		    	    		
		    	    		if (renderShadow)
		    	    		{
		    	    			minT = traceRayAndGetMinT(ICenter[0], ICenter[1], ICenter[2], icenterIP[0], icenterIP[1], icenterIP[2], null);
		    	    		}
		    	    		
		    	        	PhongIllumination.illuminate(parameter.material, parameter.materialN, l, n, v, color, (Math.abs(minT - 1.0f) > 1E-2));
		    	    	    
		    	    	    return new Color(color[0], color[1], color[2]);
	    	    	}
    	    }
		}
		
		return null;
	}
	
	private float traceRayAndGetMinT(float rayEx, float rayEy, float rayEz, float rayVx, float rayVy, float rayVz, ShadingParameter parameter)
	{
		float minT = Float.MAX_VALUE;
		RTFile scene;
		
		List<RT_Object> objects = sceneGraph.traceRayAndGetObjects(rayEx, rayEy, rayEz, rayVx, rayVy, rayVz);
		
		if (objects == null)
		{
			return minT;
		}
		
		for (int objectNumber = 0; objectNumber < objects.size(); objectNumber++)
		{
			scene = objects.get(objectNumber);
			
			// object is an implicit sphere?
	        if(scene.getHeader().equals("IMPLICIT_SPHERE"))
	        {
	        	I_Sphere sphere = (I_Sphere)scene;
              	
	            // intersection uses quadratic equation
	        	float emx, emy, emz;
	        	
	        	emx = rayEx - sphere.center[0];
	        	emy = rayEy - sphere.center[1];
	        	emz = rayEz - sphere.center[2];
	        	
	        	float ememScalar = emx * emx + emy * emy + emz * emz;
	        	float vvScalar = rayVx * rayVx + rayVy * rayVy + rayVz * rayVz;
	        	float vemScalar = rayVx * emx + rayVy * emy + rayVz * emz;
	        	
	        	// t*t * vvScalar + 2 * t * vemScalar + ememScalar - sphere.radius * sphere.radius;
	        	
	        	float a, p, q;
	        	
	        	a = vvScalar;
	        	p = (2 * vemScalar) / a;
	        	q = (ememScalar - sphere.radius * sphere.radius) / a;
	        	
	        	float d = p*p - 4*q;
	        	
	    		// no intersection point? => next object
	    		if (d <= 0)
	    		    continue;
	    		
	    		// from here: intersection takes place!
	
	    		// calculate first intersection point with sphere along the ray
	    		// x1/2 = - p / 2 +- sqrt( (p/2)^2 - q )
	    		float p2 = p / 2.0f;
	    		float t1 = (float) (- p2 + Math.sqrt( p2*p2 - q ));
	    		float t2 = (float) (- p2 - Math.sqrt( p2*p2 - q ));
	  	
	    		float t;
	    		
	    		if (t2 > t1)
	    			t = t1;
	    		else
					t = t2;
	    		
	    		// already a closer intersection point? => next object
	    		if (t > minT)
	    			continue;
	    			    		
	    		// I'm the winner until now!
	    		minT = t;
	    		
	    		// no shading parameter needed for shadow (light vector test)
	    		if (parameter != null)
	    		{
	    			parameter.object = sphere;
	    			
	    			// prepare everything for phong shading
	    			parameter.intersectionPoint[0] = rayEx + t * rayVx;
	    			parameter.intersectionPoint[1] = rayEy + t * rayVy;
	    			parameter.intersectionPoint[2] = rayEz + t * rayVz;
	    			
	    			// the normal vector at the intersection point, normalize!
	    			parameter.normal[0] = parameter.intersectionPoint[0] - sphere.center[0];
	    			parameter.normal[1] = parameter.intersectionPoint[1] - sphere.center[1];
	    			parameter.normal[2] = parameter.intersectionPoint[2] - sphere.center[2];
	    			
	    			Toolbox.normalize(parameter.normal);
	    			
	    			parameter.material = sphere.material;
	    			parameter.materialN = sphere.materialN;
	    		}
	        }
	        // object is a triangle mesh?
		    else if (scene.getHeader().equals("TRIANGLE_MESH"))
		    {
		    	T_Mesh mesh = (T_Mesh) scene;
		    	
		    	float t;
				float[] n;
				float[] ip = new float[3];
				
				float rayVn, pen;
				float[] p1, p2, p3;
				float[] baricentric = new float[3];
				
				// loop over all triangles  
				for (int i = 0; i < mesh.triangles.length; i++) 
				{			
				    // get the three vertices
				    p1 = mesh.vertices[mesh.triangles[i][0]];
				    p2 = mesh.vertices[mesh.triangles[i][1]];
				    p3 = mesh.vertices[mesh.triangles[i][2]];
				
				    // Backface culling: angle between viewing vector and normal > 90 degrees
				    // scalar product > 0
				    rayVn = rayVx *  mesh.triangleNormals[i][0] + rayVy *  mesh.triangleNormals[i][1] + rayVz *  mesh.triangleNormals[i][2];
				    
				    // backface? => next triangle
				    if (rayVn >= 0)
				    	continue;
				    
				    // no intersection point? => next triangle
				    // v * n = 0 => ray parallel to plane
				    if (Math.abs(rayVn) < 1E-7)
				    	continue;
				    
				    pen = (p1[0] - rayEx) * mesh.triangleNormals[i][0] + (p1[1] - rayEy) * mesh.triangleNormals[i][1] + (p1[2] - rayEz) * mesh.triangleNormals[i][2];
				
				    // calculate intersection point with plane along the ray
				    t = pen / rayVn;
				    
				    // already a closer intersection point? => next triangle
				    if (t >= minT)
				    	continue;	
				    
				    // the intersection point with the plane
		    		// the intersection point s(t) = RayE + t * RayV
		    		ip[0] = rayEx + t * rayVx;
		    		ip[1] = rayEy + t * rayVy;
		    		ip[2] = rayEz + t * rayVz;
		    		
		    		
		    		if (!Toolbox.triangleTest(ip, p1, p2, p3, mesh.triangleAreas[i], baricentric))
				    {
				    	continue;
				    }
		    		
		    		
		    		// from here: t < minT and triangle intersection
				    // This triangle is the winner until now!
		    		
		    		minT = t;
		    		
		    		
		    		
		    		if (parameter != null)
		    		{
		    			parameter.object = mesh;
		    			parameter.triangleIndex = i;
		    			parameter.baricentric[0] = baricentric[0];
		    			parameter.baricentric[1] = baricentric[1];
		    			parameter.baricentric[2] = baricentric[2];
		    			
		    			parameter.intersectionPoint[0] = ip[0];
		    			parameter.intersectionPoint[1] = ip[1];
		    			parameter.intersectionPoint[2] = ip[2];
		    			
		    			// set parameter used for phong shading later
		    			if (mesh.fgp == 'p' || mesh.fgp == 'P')
		    			{
		    				// the material is the material of the first triangle point 
							 int matIndex = mesh.verticesMat[mesh.triangles[i][0]];
							 parameter.material = mesh.materials[matIndex]; 
							 parameter.materialN = mesh.materialsN[matIndex]; 
		    			}
		    		}
				}
		    	
		    }
		    else // unknown object
		    {
		    	continue;	// next object
		    }
		}
		
		return minT;
	}
	
	public static void main(String[] args) throws Exception 
	{
		Raytracer rt = new Raytracer();
	}
}
 